use std::path::{PathBuf, Path};
use std::fs::File;
use std::io::{Read, Seek, Cursor};
use std::vec::IntoIter;
use std::ffi::OsStr;
// use std::io::prelude::*;

use zip::{
    ZipArchive,
    result::{ZipResult, ZipError}
};

pub const ZIP_PATH_SEPERATOR: &str = "///";

/// It's file path when the file can exist inside or outside of a zip archive.
/// Currently, only reading bytes of this file, and getting relative path are supported.
///
/// String example: ./1.zip///log/1.log
/// It points to the file 'log/1.log' inside the zip archive './1.zip'
///
/// String example: ./1.zip
/// It points to the file './1.zip'
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct PathBufWithZip {
    file_path: PathBuf,
    inner_path: Option<PathBuf>,
}

impl<T: ?Sized + AsRef<str>> From<&T> for PathBufWithZip {
    #[inline]
    fn from(s: &T) -> Self {
        let mut s_it = s.as_ref().split(ZIP_PATH_SEPERATOR).into_iter();
        let zip_file_path = s_it.next().unwrap();
        Self {
            file_path: zip_file_path.into(),
            inner_path:
                if let Some(inner_path) = s_it.next() {
                    Some(inner_path.into())
                } else {
                    None
                }
        }
    }
}

impl Into<String> for PathBufWithZip {
    #[inline]
    fn into(self) -> String {
        if let Some(inner_path_str) = &self.inner_path {
            self.file_path.to_string_lossy().to_string()
                + ZIP_PATH_SEPERATOR
                + inner_path_str.to_string_lossy().as_ref()
        } else {
            self.file_path.to_string_lossy().to_string()
        }
    }
}

// Part: Convert
impl PathBufWithZip {
    /// Convert from string path.
    #[inline]
    pub fn from_str(s: &str) -> Self {
        s.into()
    }

    /// Get its string path.
    /// It will use PathBuf::to_string_lossy().
    #[inline]
    pub fn to_string(&self) -> String {
        self.clone().into()
    }

    /// Convert into string path.
    /// It will use PathBuf::to_string_lossy().
    #[inline]
    pub fn into_string(self) -> String {
        self.into()
    }
}

// Part: Status Checker on disk
impl PathBufWithZip {
    /// Check whether the (archive) file exists.
    /// Path of the file inside the archive is not included.
    pub fn outer_file_exists(&self) -> bool {
        self.file_path.exists()
    }
}

// Part: Status Checker
impl PathBufWithZip {
    /// See std::path::PathBuf::is_file()
    pub fn is_file(&self) -> bool {
        match &self.inner_path {
            Some(inner_path) => inner_path.is_file(),
            None => self.file_path.is_file(),
        }
    }

    pub fn is_in_archive(&self) -> bool {
        self.inner_path.is_some()
    }
}

// Part Getter
impl PathBufWithZip {
    pub fn outer_file_path(&self) -> &Path {
        self.file_path.as_path()
    }

    pub fn inner_file_path(&self) -> &Path {
        self.file_path.as_path()
    }

    /// See std::path::PathBuf::file_name()
    pub fn file_name(&self) -> Option<&OsStr> {
        match &self.inner_path {
            Some(inner_path) => inner_path.file_name(),
            None => self.file_path.file_name(),
        }
    }

    /// See std::path::PathBuf::extension()
    pub fn extension(&self) -> Option<&OsStr> {
        match &self.inner_path {
            Some(inner_path) => inner_path.extension(),
            None => self.file_path.extension(),
        }
    }

    /// See std::path::PathBuf::starts_with()
    pub fn outer_starts_with<P: AsRef<Path>>(&self, base: P) -> bool {
        self.file_path.starts_with(base)
    }

    /// See std::path::PathBuf::ends_with()
    pub fn outer_ends_with<P: AsRef<Path>>(&self, base: P) -> bool {
        self.file_path.ends_with(base)
    }

    /// If inner exists, use it. Otherwise return false.
    /// See std::path::PathBuf::starts_with()
    pub fn inner_starts_with<P: AsRef<Path>>(&self, base: P) -> bool {
        match &self.inner_path {
            Some(inner_path) => inner_path.starts_with(base),
            None => false,
        }
    }

    /// If inner exists, use it. Otherwise return false.
    /// See std::path::PathBuf::ends_with()
    pub fn inner_ends_with<P: AsRef<Path>>(&self, base: P) -> bool {
        match &self.inner_path {
            Some(inner_path) => inner_path.ends_with(base),
            None => false,
        }
    }
    /// Always use outer path.
    /// See std::path::PathBuf::starts_with()
    pub fn starts_with<P: AsRef<Path>>(&self, base: P) -> bool {
        self.file_path.starts_with(base)
    }

    /// If inner exists, use it. Otherwise use outer.
    /// See std::path::PathBuf::ends_with()
    pub fn ends_with<P: AsRef<Path>>(&self, base: P) -> bool {
        match &self.inner_path {
            Some(inner_path) => inner_path.ends_with(base),
            None => self.file_path.ends_with(base),
        }
    }
}

// Part Setter
impl PathBufWithZip {
    /// See std::path::PathBuf::set_file_name()
    pub fn set_file_name<S: AsRef<OsStr>>(&mut self, file_name: S) {
        if let Some(inner_path_str) = &mut self.inner_path {
            inner_path_str.set_file_name(file_name);
        }
        else {
            self.file_path.set_file_name(file_name);
        }
    }

    /// See std::path::PathBuf::set_extension()
    pub fn set_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool {
        if let Some(inner_path_str) = &mut self.inner_path {
            inner_path_str.set_extension(extension)
        }
        else {
            self.file_path.set_extension(extension)
        }
    }
}

// Part: Cloner
impl PathBufWithZip {
    /// See std::path::PathBuf::with_file_name()
    pub fn with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Self {
        if let Some(inner_path_str) = &self.inner_path {
            Self {
                file_path: self.file_path.clone(),
                inner_path: Some(inner_path_str.with_file_name(file_name)),
            }
        } else {
            Self {
                file_path: self.file_path.with_file_name(file_name),
                inner_path: None,
            }
        }
    }

    /// See std::path::PathBuf::with_extension()
    pub fn with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Self {
        if let Some(inner_path_str) = &self.inner_path {
            Self {
                file_path: self.file_path.clone(),
                inner_path: Some(inner_path_str.with_extension(extension)),
            }
        } else {
            Self {
                file_path: self.file_path.with_extension(extension),
                inner_path: None,
            }
        }
    }

    pub fn with_new_inner_path(&self, inner_path: Option<PathBuf>) -> Self {
        Self {
            file_path: self.file_path.clone(),
            inner_path
        }
    }
}

// Part: Open Archive
impl PathBufWithZip {
    pub fn open_archive(&self) -> ZipResult<ZipArchive<File>> {
        let zip_file = File::open(self.file_path.as_path())?;
        let zip_archive = ZipArchive::new(zip_file)?;
        Ok(zip_archive)
    }

    pub fn open_archive_into_memory(&self) -> ZipResult<ZipArchive<Cursor<IntoIter<u8>>>> {
        let zip_data = std::fs::read(self.file_path.as_path())?.into_iter();
        let zip_file = Cursor::new(zip_data);
        let zip_archive = ZipArchive::new(zip_file)?;
        Ok(zip_archive)
    }
}

// Part: Reading
impl PathBufWithZip {
    pub fn read_bytes<R: Read + Seek>(
        &self,
        archive_opt: Option<&mut ZipArchive<R>>
    ) -> ZipResult<Vec<u8>> {
        match (&self.inner_path, archive_opt) {
            (Some(_inner_path), None) => Err(ZipError::FileNotFound),
            (None, Some(_archive)) => Err(ZipError::InvalidArchive("Unneed archive is provided.")),
            // Read inside archive
            (Some(inner_path), Some(archive)) => {
                let mut file = archive.by_name(&inner_path.to_string_lossy())?;
                let mut cache = Vec::new();
                cache.resize_with(file.size() as usize, Default::default);
                file.read(&mut cache)?;
                Ok(cache)
            }
            // Read outside archive
            (None, None) => Ok(std::fs::read(self.file_path.as_path())?),
        }
    }

    pub fn read_bytes_directly(&self) -> ZipResult<Vec<u8>> {
        if let Some(_) = self.inner_path {
            let mut zip_archive = self.open_archive()?;
            self.read_bytes(Some(&mut zip_archive))
        }
        else {
            self.read_bytes::<std::fs::File>(None)
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    // use std::io::prelude::*;

    #[test]
    fn path_parse() {
        let path = PathBufWithZip::from_str(&"1.zip///zip/log.txt");
        assert_eq!(path, PathBufWithZip {
            file_path: "1.zip".into(),
            inner_path: Some("zip/log.txt".into()),
        });
    }

    #[test]
    fn read() -> zip::result::ZipResult<()> {
        let file = std::fs::File::open(std::path::Path::new("test.zip")).unwrap();
        let mut zip = ZipArchive::new(file)?;
        let mut file = zip.by_name("1.txt").unwrap();
        let mut string = String::new();
        file.read_to_string(&mut string)?;
        assert_eq!(string.as_str(), "hsfipafh");
        Ok(())
    }

}

